{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "f728f24e-a7c2-4ad3-a4d7-18a41bc5f1b9",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "import torch.optim as optim\n",
    "from torch.utils.data import TensorDataset, random_split\n",
    "from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts\n",
    "from scipy.io import loadmat\n",
    "import pandas as pd\n",
    "import numpy as np"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "da6cdc6a-a81a-4342-a64e-7ba5b37ed4a8",
   "metadata": {},
   "source": [
    "# 读取数据集"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "e6bd3d05-6ce7-4941-90c8-a5ea71d27129",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'dict'>\n",
      "dict_keys(['__header__', '__version__', '__globals__', 'Label', 'feature'])\n",
      "torch.Size([48000, 2, 16, 32])\n",
      "tensor(1.)\n",
      "tensor(0.)\n"
     ]
    }
   ],
   "source": [
    "data = loadmat('../autodl-tmp/NetworkData.mat')\n",
    "print(type(data))\n",
    "print(data.keys())\n",
    "\n",
    "feature = torch.from_numpy(data['feature']).to(torch.complex64).unsqueeze(1)\n",
    "labels = torch.from_numpy(data['Label']).to(torch.complex64).unsqueeze(1)\n",
    "\n",
    "# 归一化操作：对每个通道进行 min-max 归一化\n",
    "def min_max_normalize(tensor):\n",
    "    # 按照 (C, H, W) 维度进行归一化（通常情况下 C 是第一个维度）\n",
    "    min_val = tensor.min(dim=2, keepdim=True).values  # 对每个像素点的通道（dim=2）进行最小值操作\n",
    "    max_val = tensor.max(dim=2, keepdim=True).values  # 对每个像素点的通道（dim=2）进行最大值操作\n",
    "    return (tensor - min_val) / (max_val - min_val)\n",
    "\n",
    "# 分别处理实部和虚部\n",
    "feature_real = min_max_normalize(feature.real)\n",
    "feature_imag = min_max_normalize(feature.imag)\n",
    "labels_real = min_max_normalize(labels.real)\n",
    "labels_imag = min_max_normalize(labels.imag)\n",
    "\n",
    "# 合并实部和虚部\n",
    "feature = torch.cat([feature_real, feature_imag], dim=1)\n",
    "labels = torch.cat([labels_real, labels_imag], dim=1)\n",
    "\n",
    "print(feature.shape)\n",
    "print(torch.max(feature))\n",
    "print(torch.min(feature))\n",
    "\n",
    "dataset = TensorDataset(feature, labels)\n",
    "len_dataset = len(dataset)\n",
    "# 将长度计算结果转换为整数类型\n",
    "train_size = int(0.8 * len_dataset)\n",
    "valid_size = len_dataset - train_size\n",
    "train_data, valid_data = random_split(dataset, [train_size, valid_size])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "95f44f40-dc08-4384-a008-411d762b1217",
   "metadata": {},
   "source": [
    "# 定义网络"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "f2937880-6599-400b-aa90-9d3560e6fc88",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 编码器类\n",
    "class Encoder(nn.Module):\n",
    "    def __init__(self, C_):\n",
    "        super(Encoder, self).__init__()\n",
    "        self.conv1 = nn.Conv2d(2, 16, kernel_size=4, stride=2, padding=1)\n",
    "        self.conv2 = nn.Conv2d(16, C_, kernel_size=4, stride=2, padding=1)\n",
    "    def forward(self, X):\n",
    "        Y1 = torch.relu(self.conv1(X))\n",
    "        Y = torch.relu(self.conv2(Y1))\n",
    "        return Y\n",
    "\n",
    "# 量化与二进制转换类\n",
    "class ReshapeAndQuant(nn.Module):\n",
    "    def __init__(self, q_):\n",
    "        super(ReshapeAndQuant, self).__init__()\n",
    "        self.q = q_  # 量化位数\n",
    "    def forward(self, X, v):\n",
    "        v = X.view(X.size(0), -1) # reshape\n",
    "        Z = torch.max(v) - torch.min(v)\n",
    "        mu = Z / (2 ** self.q)\n",
    "        v_quant = mu * torch.floor(v / mu)\n",
    "        v_bit = torch.round(v_quant)\n",
    "        return v_bit\n",
    "\n",
    "# 解量化与重塑类\n",
    "class DequanAndReshape(nn.Module):\n",
    "    def __init__(self, q_):\n",
    "        super(DequanAndReshape, self).__init__()\n",
    "        self.q = q_\n",
    "    def forward(self, v_bit, C_, M_, N_, N0_, Z_):\n",
    "        mu = Z_ / (2 ** self.q)\n",
    "        v_dequant = mu * v_bit\n",
    "        batch_size_ = v_bit.size(0)\n",
    "        v_reshape = v_dequant.view((batch_size_, C_, M_ // 4, N // 4 // N0)).clone()\n",
    "        return v_reshape\n",
    "\n",
    "# 残差块类\n",
    "class Residual(nn.Module):  \n",
    "    def __init__(self, input_channels, num_channels):\n",
    "        super().__init__()\n",
    "        self.conv1 = nn.Conv2d(input_channels, num_channels, kernel_size=3, stride=1, padding=1)\n",
    "        self.conv2 = nn.Conv2d(num_channels, num_channels, kernel_size=3, stride=1, padding=1) \n",
    "    def forward(self, X):\n",
    "        Y1 = F.relu(self.conv1(X))\n",
    "        Y = F.relu(self.conv2(Y1))\n",
    "        Y = Y + X  # 替代Y += X，避免就地操作\n",
    "        return Y\n",
    "# 定义残差网络块\n",
    "def resnet_block(input_channels, num_channels, num_residuals):\n",
    "    blk = []\n",
    "    for i in range(num_residuals):\n",
    "        blk.append(Residual(num_channels, num_channels))\n",
    "    return blk\n",
    "\n",
    "# 解码器类\n",
    "class Decoder(nn.Module):\n",
    "    def __init__(self, C_, B_, N0_):\n",
    "        super(Decoder, self).__init__()\n",
    "        self.tconv1 = nn.ConvTranspose2d(C_, 16, kernel_size=4, stride=2, padding=1)\n",
    "        self.tconv2 = nn.ConvTranspose2d(16, 16, kernel_size=4, stride=2, padding=1)\n",
    "        self.resblocks = nn.Sequential(*resnet_block(16, 16, B_))\n",
    "        self.upsampling = nn.ConvTranspose2d(16, 16, kernel_size=3, stride=(1, N0_), padding=1, output_padding=(0, N0_-1))\n",
    "        self.final_conv = nn.Conv2d(16, 2, kernel_size=3, stride=1, padding=1)  \n",
    "    def forward(self, X):\n",
    "        # 2 x (TConv + ReLU) \n",
    "        Y1 = F.relu(self.tconv2(F.relu(self.tconv1(X))))\n",
    "        # B x ResBlock\n",
    "        Y2 = self.resblocks(Y1)\n",
    "        # Upsampling\n",
    "        Y3 = self.upsampling(Y2)\n",
    "        # final Conv\n",
    "        Y = self.final_conv(Y3)\n",
    "        return Y\n",
    "\n",
    "# 组合网络类\n",
    "class JDCNet(nn.Module):\n",
    "    def __init__(self, encoder, reshape_and_quant,\n",
    "                 dequan_and_reshape, decoder, C_, M_, N_, N0_):\n",
    "        super(JDCNet, self).__init__()\n",
    "        self.encoder = encoder\n",
    "        self.reshape_and_quant = reshape_and_quant\n",
    "        self.dequan_and_reshape = dequan_and_reshape\n",
    "        self.decoder = decoder\n",
    "        self.C = C_\n",
    "        self.M = M_\n",
    "        self.N = N_\n",
    "        self.N0 = N0_\n",
    "    def forward(self, X):\n",
    "        # 编码器部分\n",
    "        encoded_output = self.encoder(X)\n",
    "        v_bit = self.reshape_and_quant(encoded_output, X)\n",
    "\n",
    "        # 这里假设直接通过网络获得Z，实际中经过传播后应该有另外的方法是的BS得到Z\n",
    "        Z_ = torch.max(encoded_output) - torch.min(encoded_output)\n",
    "        v_reshape = self.dequan_and_reshape(v_bit, self.C, self.M, self.N, self.N0, Z_) \n",
    "\n",
    "        # 解码器部分\n",
    "        H_hat = self.decoder(v_reshape)\n",
    "\n",
    "        return H_hat"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cd548025-634b-41b9-a51e-32bb3ef30e0d",
   "metadata": {},
   "source": [
    "# 定义损失函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "c651ebe0-c80b-4f99-9ec5-dd95b5797ade",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch.linalg as LA\n",
    "\n",
    "# 定义均方误差损失函数\n",
    "def mse_loss(predictions, targets):\n",
    "    Loss = 0\n",
    "    for i in range(len(predictions)):\n",
    "        H = torch.complex(targets[i,0], targets[i,1])\n",
    "        H_hat = torch.complex(predictions[i,0], predictions[i,1])\n",
    "        Loss += LA.norm((H_hat - H), ord=2) ** 2\n",
    "    \n",
    "    return Loss\n",
    "    \n",
    "def nmse_metric(predictions, targets):\n",
    "    mse = 0\n",
    "    for i in range(len(predictions)):\n",
    "        H = torch.complex(targets[i,0], targets[i,1])\n",
    "        H_hat = torch.complex(predictions[i,0], predictions[i,1])\n",
    "        mse += LA.norm((H_hat - H), ord=2) ** 2\n",
    "    mse = mse / len(predictions)\n",
    "    \n",
    "    var = 0\n",
    "    for i in range(len(predictions)):\n",
    "        H = torch.complex(targets[i,0], targets[i,1])\n",
    "        var += LA.norm((H), ord=2) ** 2\n",
    "    var = var / len(predictions)\n",
    "    \n",
    "    return (10 * torch.log10(mse / var)) \n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "45e24c4d-6cb5-4d15-8653-5b1b615e1789",
   "metadata": {},
   "source": [
    "# 训练及可视化"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "90f9c7e4-ebef-4ceb-b615-889789ba1413",
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import matplotlib.animation as animation\n",
    "import torch\n",
    "\n",
    "class Animator:\n",
    "    \"\"\"For plotting data in animation.\"\"\"\n",
    "    def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None,\n",
    "                 ylim=None, xscale='linear', yscale='linear',\n",
    "                 fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1,\n",
    "                 figsize=(3.5, 2.5)):\n",
    "        if legend is None:\n",
    "            legend = []\n",
    "        self.fig, self.axes = plt.subplots(nrows, ncols, figsize=figsize)\n",
    "        if nrows * ncols == 1:\n",
    "            self.axes = [self.axes, ]\n",
    "        self.config_axes = lambda: self.set_axes(\n",
    "            self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)\n",
    "        self.X, self.Y, self.fmts = None, None, fmts\n",
    "        self.lines = []\n",
    "        self.legend = legend\n",
    "\n",
    "    def set_axes(self, axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend):\n",
    "        axes.set_xlabel(xlabel)\n",
    "        axes.set_ylabel(ylabel)\n",
    "        axes.set_xscale(xscale)\n",
    "        axes.set_yscale(yscale)\n",
    "        if xlim:\n",
    "            axes.set_xlim(xlim)\n",
    "        if ylim:\n",
    "            axes.set_ylim(ylim)\n",
    "        if legend:\n",
    "            axes.legend(legend)\n",
    "\n",
    "    def add(self, x, *ys):\n",
    "        \"\"\"Add new data points to the plot.\"\"\"\n",
    "        n = len(ys)\n",
    "        if not self.X:\n",
    "            self.X = [[] for _ in range(n)]\n",
    "        if not self.Y:\n",
    "            self.Y = [[] for _ in range(n)]\n",
    "        for i, y in enumerate(ys):\n",
    "            if x is not None and y is not None:\n",
    "                self.X[i].append(x)\n",
    "                self.Y[i].append(y)\n",
    "        self.update_plot()\n",
    "\n",
    "    def update_plot(self):\n",
    "        \"\"\"Update the plot with new data.\"\"\"\n",
    "        for line in self.lines:\n",
    "            line.set_data([], [])\n",
    "        self.lines = []\n",
    "        for x, y, fmt in zip(self.X, self.Y, self.fmts):\n",
    "            line, = self.axes[0].plot(x, y, fmt)\n",
    "            self.lines.append(line)\n",
    "        self.config_axes()\n",
    "        self.fig.canvas.draw()  # Force the figure to update"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "20fe5eff-56ae-4283-9e7c-e02a221262da",
   "metadata": {},
   "outputs": [],
   "source": [
    "class CosineAnnealingWarmUpRestarts(torch.optim.lr_scheduler._LRScheduler):\n",
    "    def __init__(self, optimizer, T_0, T_w, eta_max, eta_min=5e-5, last_epoch=-1):\n",
    "        self.T_0 = T_0  # 最大调整周期\n",
    "        self.T_w = T_w  # 预热周期\n",
    "        self.eta_max = eta_max  # 最大学习率\n",
    "        self.eta_min = eta_min  # 最小学习率\n",
    "        super(CosineAnnealingWarmUpRestarts, self).__init__(optimizer, last_epoch)\n",
    "\n",
    "    def get_lr(self):\n",
    "        epoch = self.last_epoch\n",
    "        if epoch < self.T_w:\n",
    "            # 预热阶段：线性增加学习率\n",
    "            eta_t = self.eta_min + (self.eta_max - self.eta_min) * epoch / self.T_w\n",
    "        else:\n",
    "            # 余弦退火阶段：逐渐减小学习率\n",
    "            cos_inner = (epoch - self.T_w) / (self.T_0 - self.T_w)\n",
    "            cos_inner = torch.tensor(cos_inner, dtype=torch.float32)  # 将cos_inner转换为Tensor\n",
    "            cos_out = torch.cos(torch.pi * cos_inner) + 1\n",
    "            eta_t = self.eta_min + (self.eta_max - self.eta_min) / 2 * cos_out\n",
    "        return [eta_t for _ in self.optimizer.param_groups]\n",
    "\n",
    "\n",
    "def train(net, train_loader, valid_loader, num_epochs, learning_rate, weight_decay, batch_size, device):\n",
    "    net.to(device)\n",
    "    train_ls, valid_ls = [], []\n",
    "\n",
    "    # 先定义优化器\n",
    "    optimizer = torch.optim.Adam(net.parameters(), lr=learning_rate, weight_decay=weight_decay)\n",
    "    \n",
    "    # 然后定义调度器\n",
    "    scheduler = CosineAnnealingWarmUpRestarts(optimizer, T_0=200, T_w=30, eta_max=2e-3, eta_min=5e-5)\n",
    "    \n",
    "    # Initialize Animator\n",
    "    animator = Animator(xlabel='Epoch', ylabel='Loss', legend=['Train Loss', 'Valid Loss'])\n",
    "\n",
    "    for epoch in range(num_epochs):\n",
    "        net.train()\n",
    "        running_loss = 0.0\n",
    "        for X, y in train_loader:\n",
    "            X, y = X.to(device), y.to(device)\n",
    "            optimizer.zero_grad()\n",
    "            y_pred = net(X)\n",
    "            # print(y_pred.shape)\n",
    "            loss = mse_loss(y_pred, y)\n",
    "            loss.backward()\n",
    "            optimizer.step()\n",
    "            running_loss += loss.item()\n",
    "\n",
    "        train_ls.append(running_loss / len(train_loader))\n",
    "\n",
    "        # Validation loss\n",
    "        net.eval()\n",
    "        valid_loss  = 0.0\n",
    "        with torch.no_grad():\n",
    "            for X_val, y_val in valid_loader:\n",
    "                X_val, y_val = X_val.to(device), y_val.to(device)\n",
    "                y_pred_val = net(X_val)\n",
    "                valid_loss += mse_loss(y_pred_val, y_val).item()\n",
    "                \n",
    "        valid_ls.append(valid_loss / len(valid_loader))\n",
    "\n",
    "        # Update animator with the losses\n",
    "        animator.add(epoch + 1, train_ls[-1], valid_ls[-1])\n",
    "\n",
    "        # 更新学习率\n",
    "        scheduler.step()\n",
    "\n",
    "        # 打印每个epoch的损失和学习率\n",
    "        print(f\"Epoch [{epoch + 1}/{num_epochs}], train loss: {train_ls[-1]:.4f}, valid loss: {valid_ls[-1]:.4f}, LR: {scheduler.get_lr()[0]:.8f}\")\n",
    "\n",
    "    # 如果是最后一个 epoch，计算 NMSE 指标\n",
    "    if epoch == num_epochs - 1:\n",
    "        # 计算训练集的 NMSE\n",
    "        net.eval()\n",
    "        total_train_nmse = 0.0\n",
    "        with torch.no_grad():\n",
    "            for X_train, y_train in train_loader:\n",
    "                X_train, y_train = X_train.to(device), y_train.to(device)\n",
    "                y_pred_train = net(X_train)\n",
    "                total_train_nmse += nmse_metric(y_pred_train, y_train).item()\n",
    "\n",
    "        avg_train_nmse = total_train_nmse / len(train_loader)\n",
    "        print(f\"Final Train NMSE: {avg_train_nmse:.4f}\")\n",
    "\n",
    "        # 计算验证集的 NMSE\n",
    "        total_valid_nmse = 0.0\n",
    "        with torch.no_grad():\n",
    "            for X_val, y_val in valid_loader:\n",
    "                X_val, y_val = X_val.to(device), y_val.to(device)\n",
    "                y_pred_val = net(X_val)\n",
    "                total_valid_nmse += nmse_metric(y_pred_val, y_val).item()\n",
    "\n",
    "        avg_valid_nmse = total_valid_nmse / len(valid_loader)\n",
    "        print(f\"Final Valid NMSE: {avg_valid_nmse:.4f}\")\n",
    "\n",
    "    return train_ls, valid_ls\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "924387c6-090d-4f59-8651-e3aa224fe933",
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch [1/20], train loss: 80949.7612, valid loss: 18721.1377, LR: 0.00011500\n",
      "Epoch [2/20], train loss: 18441.6407, valid loss: 17955.3824, LR: 0.00018000\n",
      "Epoch [3/20], train loss: 18097.6888, valid loss: 17699.0314, LR: 0.00024500\n",
      "Epoch [4/20], train loss: 17861.1763, valid loss: 17460.4862, LR: 0.00031000\n",
      "Epoch [5/20], train loss: 17683.2475, valid loss: 17348.5370, LR: 0.00037500\n",
      "Epoch [6/20], train loss: 17609.1949, valid loss: 17302.4328, LR: 0.00044000\n",
      "Epoch [7/20], train loss: 17582.4797, valid loss: 17288.2662, LR: 0.00050500\n",
      "Epoch [8/20], train loss: 17573.6702, valid loss: 17284.3840, LR: 0.00057000\n",
      "Epoch [9/20], train loss: 17570.7009, valid loss: 17282.0207, LR: 0.00063500\n",
      "Epoch [10/20], train loss: 17568.5490, valid loss: 17279.5608, LR: 0.00070000\n",
      "Epoch [11/20], train loss: 17566.9616, valid loss: 17279.4110, LR: 0.00076500\n",
      "Epoch [12/20], train loss: 17566.0874, valid loss: 17277.4014, LR: 0.00083000\n",
      "Epoch [13/20], train loss: 17565.7014, valid loss: 17277.3594, LR: 0.00089500\n",
      "Epoch [14/20], train loss: 17565.9507, valid loss: 17279.9148, LR: 0.00096000\n",
      "Epoch [15/20], train loss: 17564.8220, valid loss: 17279.6495, LR: 0.00102500\n",
      "Epoch [16/20], train loss: 17564.6173, valid loss: 17277.7063, LR: 0.00109000\n",
      "Epoch [17/20], train loss: 17564.3322, valid loss: 17276.3290, LR: 0.00115500\n",
      "Epoch [18/20], train loss: 17564.1125, valid loss: 17275.3607, LR: 0.00122000\n",
      "Epoch [19/20], train loss: 17563.4164, valid loss: 17276.6090, LR: 0.00128500\n",
      "Epoch [20/20], train loss: 17563.7793, valid loss: 17280.1744, LR: 0.00135000\n",
      "Final Train NMSE: -8.8674\n",
      "Final Valid NMSE: -8.8805\n"
     ]
    },
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 350x250 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Hyperparameters\n",
    "C = 8\n",
    "M = 16\n",
    "N = 64\n",
    "N0 = 2  # 2 or 4\n",
    "B = 4\n",
    "q = 8\n",
    "batch_size = 256\n",
    "num_workers = 16\n",
    "epochs = 20\n",
    "lr = 0.002\n",
    "weight_decay = 0\n",
    "# 超参数设置\n",
    "eta_max = 2e-3\n",
    "eta_min = 5e-5\n",
    "T_w = 30  # 预热周期\n",
    "T_0 = 200  # 最大调整周期\n",
    "\n",
    "\n",
    "train_loader = torch.utils.data.DataLoader(train_data, \n",
    "                batch_size=batch_size, shuffle=True, \n",
    "                num_workers=num_workers, pin_memory=True)\n",
    "valid_loader = torch.utils.data.DataLoader(valid_data, \n",
    "                batch_size=batch_size, shuffle=False, \n",
    "                num_workers=num_workers, pin_memory=True)\n",
    "\n",
    "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
    "\n",
    "# Instantiate the network\n",
    "encoder = Encoder(C_=C)\n",
    "reshape_and_quant = ReshapeAndQuant(q_=q)\n",
    "dequan_and_reshape = DequanAndReshape(q_=q)\n",
    "decoder = Decoder(C_=C, B_=B, N0_=N0)\n",
    "net = JDCNet(encoder, reshape_and_quant, dequan_and_reshape, decoder,\n",
    "            C_=C, M_=M, N_=N, N0_=N0)\n",
    "\n",
    "# 在模型实例化后使用 DataParallel，实现数据并行\n",
    "net = torch.nn.DataParallel(net)\n",
    "\n",
    "net.to(device)\n",
    "\n",
    "train_net = train(net, train_loader, valid_loader, num_epochs=epochs, learning_rate=eta_max, \n",
    "      weight_decay=0, batch_size=256, device=device)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "09c9346f-c032-43b6-bd6f-4397291494a4",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
